Titanic Dataset

Fragestellung: Wie können wir effizient vorhersagen, wer überlebt hat und wer nicht? Stimmt die Behauptung, dass Frauen und Kinder zuerst gerettet wurden? TODO: Lessons learned TODO: Beteiligung, wer hat was gemacht

1) Imports:

Zuerst importieren wir notwendige Libraries und viele Elemente aus sklearn:

In [50]:
# Libraries:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
# Encoders:
pass
# Strategic imports:
pass
# Machine Learning Models:
pass
# Setups:
SEED = 42
np.random.seed(SEED)
sns.set()

2) Laden des Datensets:

Das Datenset wird geladen und erste Eindrücke gewonnen!

In [51]:
df_train = pd.read_csv('train.csv', index_col='PassengerId')
df_test = pd.read_csv('test.csv', index_col='PassengerId')

Zunächste betrachten wir die ersten Zeilen des Datensets:

In [52]:
df_train.head()
Out[52]:
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

Außerdem lassen wir uns einige Statistiken anzeigen (nur von numerischen Features):

In [53]:
df_train.describe()
Out[53]:
Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

Das Datenset hat folgende Spalten:

In [54]:
df_train.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 1 to 891
Data columns (total 11 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Survived  891 non-null    int64  
 1   Pclass    891 non-null    int64  
 2   Name      891 non-null    object 
 3   Sex       891 non-null    object 
 4   Age       714 non-null    float64
 5   SibSp     891 non-null    int64  
 6   Parch     891 non-null    int64  
 7   Ticket    891 non-null    object 
 8   Fare      891 non-null    float64
 9   Cabin     204 non-null    object 
 10  Embarked  889 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 83.5+ KB

Die Spalten haben folgende Bedeutung:

  • PassengerId: Eindeutige Identifikationsnummer des Passagiers, wurde schon als Index der Datensets verwendet und taucht deshalb hier nicht mehr auf.

  • Survived: Wer hat überlebt? Dies ist unsere Zielspalte (Label).

  • Pclass: Ticket Klasse (1, 2 oder 3) -> ordinale Skala. Dieses Feature sagt noch nichts darüber aus, wo die Zimmer auf dem Schiff waren (weiter oben an Deck, oder tiefer im Schiff? Dies könnte anhand der Cabin erklärt werden.

  • Name: Name des Passagiers. Dieser enthält auch Titel wie "Mr" oder "Mrs". Bei Frauen kann so vielleicht zwischen verheiratet ("Mrs") und unverheiratet ("Ms") unterschieden werden. Eventuell hat dies einen Einfluss auf die Überlebenswahrscheinlichkeit.

  • Sex: Entweder "male" oder "female". Sollte vor Benutzung als 0/1 kodiert werden.

  • Age: Alter des Passagiers -> Rationale Skala, aber eventuell ist eine Einteilung in Kategorien sinnvoll?

  • SibSp: Anzahl der Geschwister (Siblings) und Ehepartner (Spouses).

  • Parch: Anzahl der Eltern (Parents) und Kinder (Children) des Passagiers.

  • Ticket: String oder Zahlenfolge, die die Ticketnummer des Passagiers angibt. Eine Ticket Nummer kann sich bei verschiedenen Personen finden, die sich das Ticket also teilen.

  • Fare: Der Ticketpreis, welcher wahrscheinlich mit Deck (siehe Cabin) und Klasse (siehe Pclass) korreliert. Scheinbar bezieht sich der Preis auf das Ticket. Die Erstellung eines Features "Preis/Person" scheint daher sinnvoll.

  • Cabin: Kabinennummer (nur für sehr wenige Passagiere vorhanden). Der Buchstabe steht für das Deck, was eventuell ein wichtiges Indiz für die Evakuierbarkeit des Passagiers zulässt.

  • Embarked: Hafen, an dem der Passagier an Bord gegangen ist (drei Möglichkeiten: Southampton, Cherbourg, Queenstown). Eventuell korreliert dieser mit der Klasse (Reichtum der Bewohner an den Häfen?). Eventuell wird dieses Feature aber auch weggelassen, da es keinen großen Einfluss auf die Überlebenschancen haben sollte.

3) Explorative Datenanalyse

Zunächst schauen wir uns die einzelnen Features genauer an. Dazu legen wir erst eine Arbeitskopie des Trainingsdatensatzes an und schauen uns außerdem einen Pairplot, sowie die Korrelationsmatrix an:

In [55]:
df = df_train.copy()
sns.pairplot(df, hue='Survived', kind='scatter', diag_kind='kde', diag_kws={'bw_adjust': 0.5})
plt.show()
sns.heatmap(df.corr(), cmap='seismic_r', annot=True, center=0)
plt.show()

Auffällig ist schon jetzt, dass die Überlebensrate am stärksten mit der Pclass und Fare korreliert, welche ebenfalls beide korrelieren. Es ist zu beachten, dass kategorische Features (beispielsweise Sex) aktuell noch nicht in der Korrelationsmatrix auftauchen (dazu müssten sie erst in eine Zahlenskala transformiert werden):

In [56]:
df.replace({'male': 0, 'female': 1}, inplace=True)
sns.heatmap(df.corr(), cmap='seismic_r', annot=True, center=0)
plt.show()

Offensichtlich ist die Korrelation mit dem Geschlecht am stärksten!

3.1) Fehlende Werte

Ein wichtiger erster Schritt ist festzustellen, in welchem der Features Werte fehlen (NaN). Diese müssen dann eventuell durch Imputation-Strategien durch sinnvolle Werte ersetzt werden.

In [57]:
def check_missing_values(df):
    missing_sth = False
    for name in df.columns:
        nan_count = df[name].isnull().values.sum()
        if nan_count > 0:
            missing_sth = True
            print(f'Column "{name}" is missing {nan_count} of {df.shape[0]} values')
    if not missing_sth:
        print('No column has missing data!')

check_missing_values(df)
Column "Age" is missing 177 of 891 values
Column "Cabin" is missing 687 of 891 values
Column "Embarked" is missing 2 of 891 values

Wir sehen, dass Age recht viele fehlende Werte hat, Embarked nur zwei Stück und das Cabin knapp 3/4 aller Werte fehlen!

3.1.1) Imputation des "Age" Features

Für die Imputation kann man naiv den Median/Mittelwert aller vorhandenen Werte einsetzen. Wir können jedoch bessere Ergebnisse erzielen, wenn wir uns anschauen, welche Werte am besten mit Age korrelieren und entsprechend auffüllen:

In [58]:
df.corr()['Age'].sort_values(ascending=False, key=abs)
Out[58]:
Age         1.000000
Pclass     -0.369226
SibSp      -0.308247
Parch      -0.189119
Fare        0.096067
Sex        -0.093254
Survived   -0.077221
Name: Age, dtype: float64

In diesem Fall wäre dies das Pclass Feature. Wir könnten also prinzipiell Klassen-Mittelwerte oder Mediane für das Alter berechnen und diese für die Imputation nutzen. Wir gehen jedoch einen Schritt weiter und schauen uns ein neues Feature an, welches wir aus dem Namen generieren können (mehr dazu in Abschnitt 3.2.2):

In [59]:
df['Title'] = df['Name'].str.extract(pat='([A-Z][a-z]+\.)')
df['Title'].value_counts()
Out[59]:
Mr.          517
Miss.        182
Mrs.         125
Master.       40
Dr.            7
Rev.           6
Col.           2
Mlle.          2
Major.         2
Lady.          1
Mme.           1
Countess.      1
Don.           1
Jonkheer.      1
Ms.            1
Sir.           1
Capt.          1
Name: Title, dtype: int64

Viele Titel kommen nur selten oder ein einziges Mal vor. Wir fassen diese zusammen:

In [60]:
df['Title'][~df['Title'].isin(['Mr.', 'Miss.', 'Mrs.', 'Master.'])] = 'Misc.'
df['Title'].value_counts()
Out[60]:
Mr.        517
Miss.      182
Mrs.       125
Master.     40
Misc.       27
Name: Title, dtype: int64
In [61]:
df.groupby(['Title'])['Age'].describe()
Out[61]:
count mean std min 25% 50% 75% max
Title
Master. 36.0 4.574167 3.619872 0.42 1.000 3.5 8.00 12.0
Misc. 26.0 42.384615 13.200233 23.00 29.000 44.5 51.75 70.0
Miss. 146.0 21.773973 12.990292 0.75 14.125 21.0 30.00 63.0
Mr. 398.0 32.368090 12.708793 11.00 23.000 30.0 39.00 80.0
Mrs. 108.0 35.898148 11.433628 14.00 27.750 35.0 44.00 63.0

Wir sehen, dass die Title-Mediane deutliche Unterschiede zeigen, was unseren Ansatz bestätigt, den Titel zur Imputation zu nutzen. Im Folgenden füllen wir die fehlenden Werte mit diesen Medianen auf:

In [62]:
df['Age'].fillna(df.groupby('Title')['Age'].transform('median'), inplace=True)

Um zu zeigen, dass unser Ansatz besser als Pclass-Mediane oder der Gesamt-Median ist, schauen wir uns diese hier an:

In [63]:
df.groupby(['Pclass'])['Age'].describe()
Out[63]:
count mean std min 25% 50% 75% max
Pclass
1 216.0 37.300556 13.997633 0.92 29.0 35.0 47.25 80.0
2 184.0 29.787120 13.605174 0.67 23.0 30.0 36.00 70.0
3 491.0 25.757475 11.113889 0.42 20.0 27.0 30.00 74.0
In [64]:
df['Age'].describe()
Out[64]:
count    891.000000
mean      29.387957
std       13.262592
min        0.420000
25%       21.000000
50%       30.000000
75%       35.000000
max       80.000000
Name: Age, dtype: float64

Für "Master" (kleine Jungen) wären diese Mediane beispielsweise deutlich schlechter gewesen als die von uns gewählten.

In [65]:
check_missing_values(df)
Column "Cabin" is missing 687 of 891 values
Column "Embarked" is missing 2 of 891 values

Die Features Cabin und Embarked haben noch fehlende Werte, wir werden jedoch beide vernachlässigen, was ein Imputen überflüssig macht. In Cabin fehlen zu viele Werte (auch wenn die Deck-Nummer wahrscheinlich wertvolle Informationen enthält) und Embarked sollte keinen großen Einfluss auf das Überleben haben.

3.1.2) Imputer Funktion

Wir schreiben eine Imputer Funktion um später einfacher fehlende Werte zu ersetzen (Spalten, die nicht weiter benutzt werden, werden gedroppt, hier: Cabin und Embarked):

In [66]:
def impute(df):
    # Impute Age feature:
    temp = df.copy()
    temp['Title'] = temp['Name'].str.extract(pat='([A-Z][a-z]+\.)')
    temp['Title'][~temp['Title'].isin(['Mr.', 'Miss.', 'Mrs.', 'Master.'])] = 'Misc.'
    df['Age'].fillna(temp.groupby('Title')['Age'].transform('median'), inplace=True)
    return df

df = df_train.copy()
df = impute(df)

3.2) Sicht der Daten, Generierung neuer und Vernachlässigung unnötiger Features

Wir wollen nun einmal die einzelnen Features durchgehen und bewerten:

3.2.1) Ticket Klasse Pclass

In [67]:
sns.countplot(x='Pclass', data=df, hue='Survived')
plt.show()

Die Ticket Klasse ist ein gutes Indiz für das Überleben der Passagiere. In der dritten Klasse sinken die Chancen das Unglück zu überleben drastisch. Dieses Feature hat eine ordinale Skala und kann von uns so weiterverwendet werden. Eventuell müssen wir noch skalieren (StandardScaler oder MinMaxScaler bieten sich an).

3.2.2) Passagier Name

In [68]:
df['Name'].head(10)
Out[68]:
PassengerId
1                               Braund, Mr. Owen Harris
2     Cumings, Mrs. John Bradley (Florence Briggs Th...
3                                Heikkinen, Miss. Laina
4          Futrelle, Mrs. Jacques Heath (Lily May Peel)
5                              Allen, Mr. William Henry
6                                      Moran, Mr. James
7                               McCarthy, Mr. Timothy J
8                        Palsson, Master. Gosta Leonard
9     Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)
10                  Nasser, Mrs. Nicholas (Adele Achem)
Name: Name, dtype: object

Name enthält den Namen der Passagiere und hat dementsprechend eine Nominalskala. Wir haben stichprobenartig untersucht, ob die Cross channel Passagiere (siehe: https://en.wikipedia.org/wiki/Passengers_of_the_Titanic) im Datensatz vorkommen (wir haben außergewöhnliche/auffällige Namen genutzt):

In [69]:
cross_channel_samples = ['DeGrasse', 'Dyer-Edwardes', 'Lenox-Conyngham', 'Osborne', 'Remesch']
if df['Name'].str.contains('|'.join(cross_channel_samples)).any():
    print('(Some) cross channel passengers are included!')
else:
    print('No cross channel passengers were found!')
No cross channel passengers were found!

Obwohl wir den Namen selbst nicht benutzen können, ist es uns möglich ein interessantes Feature aus dem Datensatz zu extrahieren: den Titel der Person! Dieser wird immer groß geschrieben und endet in einem Punkt und wir können ihn mit einer Regular Expression erfassen (dieses Feature wurde schon in Abschnitt 3.1.1 zur Imputation benutzt und wird hier weiter erklärt):

In [70]:
df['Title'] = df['Name'].str.extract(pat='([A-Z][a-z]+\.)')
df['Title'].value_counts()
Out[70]:
Mr.          517
Miss.        182
Mrs.         125
Master.       40
Dr.            7
Rev.           6
Col.           2
Mlle.          2
Major.         2
Lady.          1
Mme.           1
Countess.      1
Don.           1
Jonkheer.      1
Ms.            1
Sir.           1
Capt.          1
Name: Title, dtype: int64

Sehr viele der selteneren Titel tauchen nur wenige Male auf und werden von uns zu Misc. (Miscellaneous = Verschiedenes) zusammengefasst:

In [71]:
df['Title'][~df['Title'].isin(['Mr.', 'Miss.', 'Mrs.', 'Master.'])] = 'Misc.'
df[df['Title'].isin(['Misc.'])].head()
Out[71]:
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Title
PassengerId
31 0 1 Uruchurtu, Don. Manuel E male 40.0 0 0 PC 17601 27.7208 NaN C Misc.
150 0 2 Byles, Rev. Thomas Roussel Davids male 42.0 0 0 244310 13.0000 NaN S Misc.
151 0 2 Bateman, Rev. Robert James male 51.0 0 0 S.O.P. 1166 12.5250 NaN S Misc.
246 0 1 Minahan, Dr. William Edward male 44.0 2 0 19928 90.0000 C78 Q Misc.
250 0 2 Carter, Rev. Ernest Courtenay male 54.0 1 0 244252 26.0000 NaN S Misc.
In [72]:
df['Title'].value_counts()
Out[72]:
Mr.        517
Miss.      182
Mrs.       125
Master.     40
Misc.       27
Name: Title, dtype: int64

Schauen wir uns die Misc.-Titel ein wenig genauer im Hinblick auf die Überlebensrate an:

In [73]:
df[df['Title'] == 'Misc.'].sort_values(by='Survived')
Out[73]:
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Title
PassengerId
31 0 1 Uruchurtu, Don. Manuel E male 40.0 0 0 PC 17601 27.7208 NaN C Misc.
823 0 1 Reuchlin, Jonkheer. John George male 38.0 0 0 19972 0.0000 NaN S Misc.
767 0 1 Brewe, Dr. Arthur Jackson male 44.5 0 0 112379 39.6000 NaN C Misc.
746 0 1 Crosby, Capt. Edward Gifford male 70.0 1 1 WE/P 5735 71.0000 B22 S Misc.
695 0 1 Weir, Col. John male 60.0 0 0 113800 26.5500 NaN S Misc.
849 0 2 Harper, Rev. John male 28.0 0 1 248727 33.0000 NaN S Misc.
537 0 1 Butt, Major. Archibald Willingham male 45.0 0 0 113050 26.5500 B38 S Misc.
627 0 2 Kirkland, Rev. Charles Leonard male 57.0 0 0 219533 12.3500 NaN Q Misc.
887 0 2 Montvila, Rev. Juozas male 27.0 0 0 211536 13.0000 NaN S Misc.
150 0 2 Byles, Rev. Thomas Roussel Davids male 42.0 0 0 244310 13.0000 NaN S Misc.
151 0 2 Bateman, Rev. Robert James male 51.0 0 0 S.O.P. 1166 12.5250 NaN S Misc.
318 0 2 Moraweck, Dr. Ernest male 54.0 0 0 29011 14.0000 NaN S Misc.
246 0 1 Minahan, Dr. William Edward male 44.0 2 0 19928 90.0000 C78 Q Misc.
250 0 2 Carter, Rev. Ernest Courtenay male 54.0 1 0 244252 26.0000 NaN S Misc.
399 0 2 Pain, Dr. Alfred male 23.0 0 0 244278 10.5000 NaN S Misc.
797 1 1 Leader, Dr. Alice (Farnham) female 49.0 0 0 17465 25.9292 D17 S Misc.
760 1 1 Rothes, the Countess. of (Lucy Noel Martha Dye... female 33.0 0 0 110152 86.5000 B77 S Misc.
711 1 1 Mayne, Mlle. Berthe Antonine ("Mrs de Villiers") female 24.0 0 0 PC 17482 49.5042 C90 C Misc.
444 1 2 Reynaldo, Ms. Encarnacion female 28.0 0 0 230434 13.0000 NaN S Misc.
642 1 1 Sagesser, Mlle. Emma female 24.0 0 0 PC 17477 69.3000 B35 C Misc.
633 1 1 Stahelin-Maeglin, Dr. Max male 32.0 0 0 13214 30.5000 B50 C Misc.
600 1 1 Duff Gordon, Sir. Cosmo Edmund ("Mr Morgan") male 49.0 1 0 PC 17485 56.9292 A20 C Misc.
557 1 1 Duff Gordon, Lady. (Lucille Christiana Sutherl... female 48.0 1 0 11755 39.6000 A16 C Misc.
370 1 1 Aubart, Mme. Leontine Pauline female 24.0 0 0 PC 17477 69.3000 B35 C Misc.
450 1 1 Peuchen, Major. Arthur Godfrey male 52.0 0 0 113786 30.5000 C104 S Misc.
648 1 1 Simonius-Blumer, Col. Oberst Alfons male 56.0 0 0 13213 35.5000 A26 C Misc.
661 1 1 Frauenthal, Dr. Henry William male 50.0 2 0 PC 17611 133.6500 NaN S Misc.

Interessant ist, dass (wie bereits in 3.1.1 gezeigt), das Alter der Passagiere mit seltenen Titeln sehr hoch ist, was sich mit dem hohen Rang (Militär) oder geistlichen Würden (z.B. "Rev." für "Reverend"), sowie Adelsstand (z.B. "Countess") begründen lässt.

In [74]:
sns.countplot(x='Sex', data=df[df['Title'] == 'Misc.'], hue='Survived')
Out[74]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c4d5e9a288>

Außerdem kann man sehen, dass alle Frauen dieser Kategorie überlebt haben, von den Männern aber überdurchschnittlich viele nicht. Wir schätzen dieses Feature von daher als recht wichtig ein.

Zu guter Letzt wandeln wir die immer noch nominale Skala des Titels in 5 binäre Features um, die von unseren Algorithmen verwendet werden können:

In [75]:
df = pd.get_dummies(df, columns=['Title'])
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 1 to 891
Data columns (total 16 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   Survived       891 non-null    int64  
 1   Pclass         891 non-null    int64  
 2   Name           891 non-null    object 
 3   Sex            891 non-null    object 
 4   Age            891 non-null    float64
 5   SibSp          891 non-null    int64  
 6   Parch          891 non-null    int64  
 7   Ticket         891 non-null    object 
 8   Fare           891 non-null    float64
 9   Cabin          204 non-null    object 
 10  Embarked       889 non-null    object 
 11  Title_Master.  891 non-null    uint8  
 12  Title_Misc.    891 non-null    uint8  
 13  Title_Miss.    891 non-null    uint8  
 14  Title_Mr.      891 non-null    uint8  
 15  Title_Mrs.     891 non-null    uint8  
dtypes: float64(2), int64(4), object(5), uint8(5)
memory usage: 120.2+ KB

3.2.3) Geschlecht Sex

In [76]:
sns.countplot(x='Sex', data=df, hue='Survived')
plt.show()

Das Geschlecht hat einen sehr starken Einfluss auf die Überlebenschancen! Aktuell hat dieses Feature eine Nominalskala ("male"/"female") und wird von uns in ein binäres Feature umgewandelt:

In [77]:
df.replace({'male': 0, 'female': 1}, inplace=True)
df['Sex'].value_counts()
Out[77]:
0    577
1    314
Name: Sex, dtype: int64

3.2.4) Alter Age

Dieses Feature zeigt das Alter der Passagiere an.

In [78]:
sns.histplot(x='Age', data=df, hue='Survived')
Out[78]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c4d621d108>

Aus dem Histogramm lässt sich erkennen, dass junge Kinder viel höhere Überlebenschancen hatten als Erwachsene. Sehr auffällig ist der hohe Anteil von Ertrinkenden bei den ca. 30-jährigen. Entsprechend dieser Statistik haben wir uns für eine Kategorisierung der Daten entschieden. Ein paar Entscheidungskriterien:

  • 5 war das Einschulungsalter zur damaligen Zeit.

  • Der älteste "Master" im Datenset ist 12 Jahre alt.

  • Volljährigkeit mit 18.

  • Peaks im Graph bei ca. 20 und ca. 30 rechtfertigen Abschnitte von 18-25, sowie von 35 bis 45.

  • Sehr alte Menschen (über 60) scheinen keine hohen Überlebenschancen zu haben.

In [79]:
df['AgeCat'] = pd.cut(df['Age'], bins=[0, 5, 12, 18, 25, 35, 60, np.inf], labels=[1, 2, 3, 4, 5, 6, 7]).astype(int)
sns.countplot(x='AgeCat', data=df, hue='Survived')
Out[79]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c4d6382d88>

3.2.5) SibSp und Parch

Diese Features beschreiben die Anzahl der Geschwister (Siblings) und Ehepartner (Spouses), sowie die Anzahl der Eltern (Parents) und Kinder (Children):

In [80]:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
sns.countplot(x='SibSp', data=df, hue='Survived', ax=axes[0])
sns.countplot(x='Parch', data=df, hue='Survived', ax=axes[1])
Out[80]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c4d642df08>

Beide Features zeigen einen sehr ähnlichen Zusammenhang zur Überlebensrate und werden von uns von daher zur Familiengröße ´FamilySize´ zusammengefasst:

In [81]:
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1  # +1: die Person selbst wird mit eingerechnet!
df['FamilySize'].value_counts()
Out[81]:
1     537
2     161
3     102
4      29
6      22
5      15
7      12
11      7
8       6
Name: FamilySize, dtype: int64

Eine interessante Entdeckung war ein Geschwisterpaar (SibSp=1), welches ohne Eltern (Parch=0) auf Reisen waren. Beide (12 und 14 Jahre) haben überlebt!

In [82]:
df[df['Ticket']=='2651']
Out[82]:
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Title_Master. Title_Misc. Title_Miss. Title_Mr. Title_Mrs. AgeCat FamilySize
PassengerId
40 1 3 Nicola-Yarred, Miss. Jamila 1 14.0 1 0 2651 11.2417 NaN C 0 0 1 0 0 3 2
126 1 3 Nicola-Yarred, Master. Elias 0 12.0 1 0 2651 11.2417 NaN C 1 0 0 0 0 2 2

3.2.6) Ticketnummer Ticket

Die Ticketnummer kann von uns nicht direkt verwendet werden, da es scheinbar kein eindeutiges System für die Zahlen und Buchstaben gibt. Man kann weder die Kabine, noch das Deck ableiten und es scheint, dass unterschiedliche Ausgabestellen andere Konventionen verwenden. Was wir jedoch tun können, ist die Ticketnummer zu verwenden um Passagiere zusammenzufassen, die zusammen gereist sind. Wir führen deshalb das neue Feature GroupSize ein welches nicht unähnlich zur FamilySize ist (allerdings können auch Freunde zusammen reisen und Familien können mehrere Tickets nutzen, es besteht also ein Mehrwert dieses Features):

In [83]:
df['GroupSize'] = df['Ticket'].map(df['Ticket'].value_counts())
df['GroupSize'].value_counts()
Out[83]:
1    547
2    188
3     63
4     44
7     21
6     18
5     10
Name: GroupSize, dtype: int64

Im Folgenden zeigen wir die ersten drei Reisegruppen mit dem Maximum von 7 Personen in GroupSize:

  • Ticket=CA. 2434: Familie Sage mit 11 Personen, die also mehrere Tickets besaßen. Alle 7 von diesem Ticket starben.

  • Ticket=347082: Familie Andersson mit 7 Personen, alle mit diesem Ticket. Alle starben.

  • Ticket=1601: Eine asiatische Reisegruppe, die nicht verwandt war, von denen 5 überlebten.

    Für die erste Gruppe gibt es leider keine gute Möglichkeit die anderen 4 Familienmitglieder (auf mindestens einem weiteren Ticket) ausfindig zu machen um zu überprüfen ob diese überlebt haben.

In [84]:
columns = ['Name', 'Ticket', 'GroupSize', 'FamilySize', 'Survived']
df[columns].sort_values(by=['GroupSize', 'Ticket'], ascending=False).head(21)
Out[84]:
Name Ticket GroupSize FamilySize Survived
PassengerId
160 Sage, Master. Thomas Henry CA. 2343 7 11 0
181 Sage, Miss. Constance Gladys CA. 2343 7 11 0
202 Sage, Mr. Frederick CA. 2343 7 11 0
325 Sage, Mr. George John Jr CA. 2343 7 11 0
793 Sage, Miss. Stella Anna CA. 2343 7 11 0
847 Sage, Mr. Douglas Bullen CA. 2343 7 11 0
864 Sage, Miss. Dorothy Edith "Dolly" CA. 2343 7 11 0
14 Andersson, Mr. Anders Johan 347082 7 7 0
120 Andersson, Miss. Ellis Anna Maria 347082 7 7 0
542 Andersson, Miss. Ingeborg Constanzia 347082 7 7 0
543 Andersson, Miss. Sigrid Elisabeth 347082 7 7 0
611 Andersson, Mrs. Anders Johan (Alfrida Konstant... 347082 7 7 0
814 Andersson, Miss. Ebba Iris Alfrida 347082 7 7 0
851 Andersson, Master. Sigvard Harald Elias 347082 7 7 0
75 Bing, Mr. Lee 1601 7 1 1
170 Ling, Mr. Lee 1601 7 1 0
510 Lang, Mr. Fang 1601 7 1 1
644 Foo, Mr. Choong 1601 7 1 1
693 Lam, Mr. Ali 1601 7 1 1
827 Lam, Mr. Len 1601 7 1 0
839 Chip, Mr. Chang 1601 7 1 1

3.2.7) Ticketpreis Fare

In [85]:
df.groupby(['Pclass'])['Fare'].describe()
Out[85]:
count mean std min 25% 50% 75% max
Pclass
1 216.0 84.154687 78.380373 0.0 30.92395 60.2875 93.5 512.3292
2 184.0 20.662183 13.417399 0.0 13.00000 14.2500 26.0 73.5000
3 491.0 13.675550 11.778142 0.0 7.75000 8.0500 15.5 69.5500

Der Ticketpreis variiert sehr stark bis hin zu 512$ in der 1. Klasse. Auffällig ist auch, dass scheinbar Leute umsonst mitgefahren sind (0$ Minimum in allen 3 Klassen). Ein weiteres Problem ist, dass die Preise pro Ticket und nicht pro Person angegeben sind. Dies korrigieren wir im Folgenden:

In [86]:
df['PersonPerTicket'] = df['Ticket'].map(df['Ticket'].value_counts())
df['FarePerPerson'] = df['Fare'] / df['PersonPerTicket']

Um weitere Aussagen machen zu können schauen wir auf ein Histogramm:

In [87]:
sns.histplot(x='FarePerPerson', data=df, hue='Survived')
plt.show()

Die extrem hohen Preise scheinen sehr starke Ausreißer zu sein, was das Lesen des Plots erschwert. Wir beschränken deshalb die Plotting-Range in x. Zusätzlich schneiden beschränken wir auch y um höhere Preise besser untersuchen zu können:

In [88]:
sns.histplot(x='FarePerPerson', data=df, hue='Survived')
plt.xlim(0, 150)
plt.ylim(0, 50)
plt.show()

Auffällig viele Passagiere in den niedrigen Preisklassen haben die Reise nicht überlebt. Da der Preis mit der Klasse korrelieren sollte, ist dies jedoch nicht verwunderlich. Jedoch scheinen Leute mit sehr hohen Ticketpreisen sehr gute Chancen zu haben. Von den Leuten, die 0$ gezahlt haben verunglückten die meisten! Aufgrund dieser Überlegungen legen wir auch für Fare Kategorien fest, die wir in einem neuen Feature FareCat speichern.

In [89]:
df['FareCat'] = pd.cut(df['FarePerPerson'], bins=[-1, 1, 10, 20, 30, 50, np.inf], labels=[1, 2, 3, 4, 5, 6]).astype(int)
sns.countplot(x='FareCat', data=df, hue='Survived')
plt.show()
df.groupby(['Survived'])['FareCat'].value_counts()
Out[89]:
Survived  FareCat
0         2          351
          3          102
          4           36
          5           30
          6           16
          1           14
1         2          121
          3           72
          4           66
          5           43
          6           39
          1            1
Name: FareCat, dtype: int64

Unsere Einteilung zeigt sogar, das nur eine Person mit einem Preis von 0$ überlebt hat!

In [90]:
df[(df['FareCat'] == 1) & (df['Survived'] == 1)]
Out[90]:
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin ... Title_Misc. Title_Miss. Title_Mr. Title_Mrs. AgeCat FamilySize GroupSize PersonPerTicket FarePerPerson FareCat
PassengerId
272 1 3 Tornquist, Mr. William Henry 0 25.0 0 0 LINE 0.0 NaN ... 0 0 1 0 4 1 4 4 0.0 1

1 rows × 22 columns

ANMERKUNG: Wenn nicht alle Passagiere eines Tickets im Trainingsset sind (z.B. teilweise im Testset oder gar nicht vorhanden), dann kann von der ermittelten Zahl der Personen pro Ticket nicht exakt auf den pro-Kopf-Preis geschlossen werden. Dies wird von uns hier jedoch vernachlässigt.

3.2.8) Kabinennummer Cabin

Cabin: Kabinennummer (nur für sehr wenige Passagiere vorhanden). Der Buchstabe steht für das Deck, was eventuell ein wichtiges Indiz für die Evakuierbarkeit des Passagiers zulässt.

In [90]:

3.2.9) Starthafen Embarked

Embarked: Hafen, an dem der Passagier an Bord gegangen ist (drei Möglichkeiten: Southampton, Cherbourg, Queenstown). Eventuell korreliert dieser mit der Klasse (Reichtum der Bewohner an den Häfen?). Eventuell wird dieses Feature aber auch weggelassen, da es keinen großen Einfluss auf die Überlebenschancen haben sollte.

In [91]:

3.2.10) FeatureTransformer Klasse

Im Folgenden fassen wir unsere Überlegungen über die ursprünglichen Features des Datensets in einer Transformer-Klasse zusammen, welche uns vorhandene Features bei Bedarf umformt, neue Features erstellt und zu guter Letzt Features droppt, die wir nicht für die Modelle benötigen: FUNCTION STARTS HERE:

In [91]:
def feature_engineer(df):
    # Create new `Title` feature and create a new numeric feature for each different title:
    df['Title'] = df['Name'].str.extract(pat='([A-Z][a-z]+\.)')
    df['Title'][~df['Title'].isin(['Mr.', 'Miss.', 'Mrs.', 'Master.'])] = 'Misc.'
    df = pd.get_dummies(df, columns=['Title'])
    # Replace `Sex` string entries with 0/1:
    df.replace({'male': 0, 'female': 1}, inplace=True)
    # Categorize `Age` Feature:
    df['AgeCat'] = pd.cut(df['Age'], bins=[0, 5, 12, 18, 25, 35, 60, np.inf], labels=[1, 2, 3, 4, 5, 6, 7]).astype(int)
    # Create new feature `FamilySize`:
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1  # +1: Person itself
    # Create new feature `GroupSize` (not necessarily relatives):
    df['GroupSize'] = df['Ticket'].map(df['Ticket'].value_counts())
    # Categorize `Fare` Feature:
    df['PersonPerTicket'] = df['Ticket'].map(df['Ticket'].value_counts())
    df['FarePerPerson'] = df['Fare'] / df['PersonPerTicket']
    bins, labels = [-1, 1, 10, 20, 30, 50, np.inf], [1, 2, 3, 4, 5, 6]
    df['FareCat'] = pd.cut(df['FarePerPerson'], bins=bins, labels=labels).astype(int)
    # Drop all non-used features:
    drop = ['Age', 'Name', 'Cabin', 'Embarked', 'SibSp', 'Parch', 'Ticket', 'PersonPerTicket', 'Fare', 'FarePerPerson']
    df.drop(drop, axis=1, inplace=True)
    return(df)

df = df_train.copy()
df = impute(df)
df = feature_engineer(df)
df.info()
df.describe()




########################################################################################################################

# Zielwerte abtrennen

survival_target = np.array(df["Survived"]).copy()
# reshape um es später an Algorithmen weiterzugeben.# droppen von survived
#survival_target = np.reshape(survival_target,(survival_target.shape[0],1) )
df.drop(['Survived'],
        axis=1, inplace=True)



feature_matrix = df
print(df.columns)

# Scaling mit den Features
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaled_features = scaler.fit(feature_matrix)
scaled_features = scaler.transform(feature_matrix)


# split train/test: Auftrennen in Trainings und Validationsmenge(test=validation)

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    scaled_features, survival_target, test_size=0.2, random_state=0)
print(X_train)
print(X_train.shape)

################################

# PCA um Features rauszuwerfen
# eventuell nicht notwendig, da wenige Daten




################################




# erstes Modell (fit, transform, predict (mit test und train?) und Auswertung(Score?)
from sklearn.tree import DecisionTreeClassifier, plot_tree

clf = DecisionTreeClassifier(max_depth=4)
#y_train = y_train.flatten()
print(X_train.shape)
print(y_train.shape)
model = clf.fit(X_train, y_train)
print("score train: ", model.score(X_train, y_train))
print("score test: ", model.score(X_test, y_test))
<class 'pandas.core.frame.DataFrame'>
Int64Index: 891 entries, 1 to 891
Data columns (total 12 columns):
 #   Column         Non-Null Count  Dtype
---  ------         --------------  -----
 0   Survived       891 non-null    int64
 1   Pclass         891 non-null    int64
 2   Sex            891 non-null    int64
 3   Title_Master.  891 non-null    uint8
 4   Title_Misc.    891 non-null    uint8
 5   Title_Miss.    891 non-null    uint8
 6   Title_Mr.      891 non-null    uint8
 7   Title_Mrs.     891 non-null    uint8
 8   AgeCat         891 non-null    int64
 9   FamilySize     891 non-null    int64
 10  GroupSize      891 non-null    int64
 11  FareCat        891 non-null    int64
dtypes: int64(7), uint8(5)
memory usage: 92.3 KB
Index(['Pclass', 'Sex', 'Title_Master.', 'Title_Misc.', 'Title_Miss.',
       'Title_Mr.', 'Title_Mrs.', 'AgeCat', 'FamilySize', 'GroupSize',
       'FareCat'],
      dtype='object')
[[1.         1.         0.         ... 0.2        0.16666667 0.2       ]
 [0.5        0.         0.         ... 0.         0.         0.4       ]
 [0.5        0.         0.         ... 0.2        0.16666667 0.4       ]
 ...
 [1.         0.         0.         ... 0.         0.         0.2       ]
 [1.         1.         0.         ... 0.1        0.         0.4       ]
 [0.5        0.         0.         ... 0.2        0.16666667 0.4       ]]
(712, 11)
(712, 11)
(712,)
score train:  0.8342696629213483
score test:  0.8044692737430168
In [92]:
with plt.style.context('classic'):
    plt.figure(figsize=(16,10))
    plot_tree(model)
    plt.show()
In [93]:
# mit cross validation (Test im Trainings-Set):
from sklearn.model_selection import cross_val_score
clf2 = DecisionTreeClassifier(max_depth=4)
scores = cross_val_score(clf2, X_train, y_train, cv=5)
print("Score mit cross_val: ", scores)
print("Score cross_val Mittelwert",scores.mean())


#random forest????


# eventuell grid search für verschiedene parameter im DecisionTree
from sklearn.model_selection import GridSearchCV

#Parameter für DesicionTree Gridsearch: unterschiedliche Tiefen und Entscheidungskriterien
param_grid = {"max_depth":[3, 4, 5, 6, 7, 8, 10, 20], "criterion":["gini", "entropy"]}
decision_tree = DecisionTreeClassifier()
grid_search = GridSearchCV(decision_tree, param_grid, return_train_score=True, cv=6)
grid_search.fit(X_train, y_train)
print(grid_search)
print('Bester Parameter Gridsearch: ', grid_search.best_params_)
cvres = grid_search.cv_results_

#print(cvres)

for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print((mean_score), params)


# Bayes
from sklearn.naive_bayes import CategoricalNB
bayes_clf = CategoricalNB()
model = bayes_clf.fit(X_train, y_train)
print("score train Bayes: ", model.score(X_train, np.ravel(y_train)))
print("score test Bayes: ", model.score(X_test, np.ravel(y_test)))



# Knearest

from sklearn.neighbors import KNeighborsClassifier
KN_clf = KNeighborsClassifier(weights = 'distance')
model = KN_clf.fit(X_train, y_train)
print("score train KN_clf (weights = distance): ", model.score(X_train, y_train))
print("score test KN_clf (weights = distance): ", model.score(X_test, y_test))

KN_clf = KNeighborsClassifier(weights = 'uniform')
model = KN_clf.fit(X_train, y_train)
print("score train KN_clf (weights = uniform): ", model.score(X_train, y_train))
print("score test KN_clf (weights = uniform): ", model.score(X_test, y_test))
Score mit cross_val:  [0.81818182 0.84615385 0.78873239 0.79577465 0.86619718]
Score cross_val Mittelwert 0.8230079779375554
GridSearchCV(cv=6, estimator=DecisionTreeClassifier(),
             param_grid={'criterion': ['gini', 'entropy'],
                         'max_depth': [3, 4, 5, 6, 7, 8, 10, 20]},
             return_train_score=True)
Bester Parameter Gridsearch:  {'criterion': 'gini', 'max_depth': 3}
0.8216778236718416 {'criterion': 'gini', 'max_depth': 3}
0.8146750225513935 {'criterion': 'gini', 'max_depth': 4}
0.8188648340692208 {'criterion': 'gini', 'max_depth': 5}
0.7977970849356693 {'criterion': 'gini', 'max_depth': 6}
0.7991739068508759 {'criterion': 'gini', 'max_depth': 7}
0.7977733466267862 {'criterion': 'gini', 'max_depth': 8}
0.800586336229407 {'criterion': 'gini', 'max_depth': 10}
0.8019750272990551 {'criterion': 'gini', 'max_depth': 20}
0.8202653942933106 {'criterion': 'entropy', 'max_depth': 3}
0.8118620329487728 {'criterion': 'entropy', 'max_depth': 4}
0.8202653942933106 {'criterion': 'entropy', 'max_depth': 5}
0.8033993258320278 {'criterion': 'entropy', 'max_depth': 6}
0.8090134358828278 {'criterion': 'entropy', 'max_depth': 7}
0.7879456867492761 {'criterion': 'entropy', 'max_depth': 8}
0.7977614774723448 {'criterion': 'entropy', 'max_depth': 10}
0.7991739068508759 {'criterion': 'entropy', 'max_depth': 20}
score train Bayes:  0.797752808988764
score test Bayes:  0.7541899441340782
score train KN_clf (weights = distance):  0.8890449438202247
score test KN_clf (weights = distance):  0.8324022346368715
score train KN_clf (weights = uniform):  0.8525280898876404
score test KN_clf (weights = uniform):  0.8100558659217877